package jsonreader;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;

public class JSONParser
{
	private JSONParser()
	{
		// Singleton
	}
	
	public static JSONValue parseFile(String fileName) throws JSONParseException, FileNotFoundException
	{
		Scanner scan = null;
		// We want to close the scanner if it's opened no matter what happens, so
		// this is enclosed in a try(-catch-)finally statement
		try
		{
			scan = new Scanner(new File(fileName));
			
			return JSONParser.parseString(scan.useDelimiter("\\Z").next());
		}
		finally
		{
			if(scan != null)
			{
				scan.close();
			}
		}
		
		
	}
	
	/**
	 * JSON Spec taken from http://tools.ietf.org/html/rfc4627
	 * @throws Exception on invalid json
	 */
	public static JSONValue parseString(String json) throws JSONParseException
	{
		for(int i=0; i<json.length(); i++)
		{
			JSONType type = null;
			
			switch(json.charAt(i))
			{
			// We're using the fact that you continue executing past labels in switch statements to halve the code
			case '{':
			{
				type = JSONType.OBJECT;
				
				ArrayList<JSONValue> arrayContents = JSONParser.processObject(json.substring(i), '{', '}');
				
				HashMap<String, JSONValue> hashMap = new HashMap<String, JSONValue>();
				for(JSONValue value : arrayContents)
				{
					JSONKeyValuePair pair = value.getKeyValuePair();
					if(pair == null)
					{
						throw new JSONParseException("Invalid JSON input. Object's must contain key value pairs");
					}
					
					hashMap.put(pair.key, pair.value);
				}
				
				return new JSONValue(type, hashMap);
			}
			case '[':
			{
				type = JSONType.ARRAY;
				ArrayList<JSONValue> arrayContents = JSONParser.processObject(json.substring(i), '[', ']');
				
				// We could check for consistent types, however Javascript lets you have array's of [0, null, "blah"] so we won't
				for(JSONValue value : arrayContents)
				{
					if(value.getType() == JSONType.KEYVALUE)
					{
						throw new JSONParseException("Arrays cannot store key value pairs. Use an object instead");
					}
				}
				
				return new JSONValue(type, arrayContents.toArray(new JSONValue[arrayContents.size()]));
			}
			case '\"':
			// Either string, or key value pair.
			{
				boolean foundEndQuote = false;
				int j = i;
				while(!foundEndQuote && (j+1) < json.length())
				{
					j++;
					if(json.charAt(j) == '\"' && j != 0 && json.charAt(j-1) != '\\')
					{
						foundEndQuote = true;
					}
				}
				if(!foundEndQuote)
				{
					throw new JSONParseException("Invalid JSON file. Unable to match end string quote");
				}
				
				String key = json.substring(i+1, j);
				
				String value = json.substring(j+1).trim();
				if(value.length() > 0 && value.charAt(0) == ':')
				{
					return new JSONValue(JSONType.KEYVALUE, new JSONKeyValuePair(key, JSONParser.parseString(value.substring(1))));
				}
				else
				{
					return new JSONValue(JSONType.STRING, key);
				}
			}
			
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
			case '.':
			case '-':
			{
				// We split the string by either a JSON token value, or whitespace.
				String doubleString = json.substring(i).split("[{}\\[\\],:\\s]")[0];
				
				Double value = null;
				
				try
				{
					value = Double.parseDouble(doubleString);
				}
				catch(NumberFormatException e)
				{
					throw new JSONParseException("Invalid number format");
				}
				
				return new JSONValue(JSONType.NUMBER, value);
			}
			case 't':
			{
				if((json.length() >= i+4) && json.substring(i, i+4).equals("true"))
				{
					return new JSONValue(JSONType.BOOLEAN, true);
				}
				throw new JSONParseException("Invalid JSON file");
			}
			case 'f':
			{
				if((json.length() >= i+5) && json.substring(i, i+5).equals("false"))
				{
					return new JSONValue(JSONType.BOOLEAN, false);
				}
				throw new JSONParseException("Invalid JSON file");
			}
			case 'n':
			{
				if((json.length() >= i+4) && json.substring(i, i+4).equals("null"))
				{
					return new JSONValue(JSONType.NULL, null);
				}
				throw new JSONParseException("Invalid JSON file");
			}
			}
		}
		throw new JSONParseException("Invalid JSON input. Nothing to be parsed");
	}
	
	private static ArrayList<JSONValue> processObject(String jsonStringFromObjectStart, char levelIncreaseChar, char levelDecreaseChar) throws JSONParseException
	{
		if(jsonStringFromObjectStart.length() < 2)
		{
			throw new JSONParseException("Invalid object or table formatting. Expecting more characters");
		}
		
		// We are in an array/object, we want to split it's contents up based on commas. However we can't get confused between commas inside of other objects / arrays
		ArrayList<JSONValue> arrayContents = new ArrayList<JSONValue>();
		
		int level = 0;
		int j = 0;
		int lastSeenComma = 1;
		
		while(level >= 0 && j < jsonStringFromObjectStart.length())
		{
			j++;
			char stringCharAt = jsonStringFromObjectStart.charAt(j);
			
			if(stringCharAt == levelIncreaseChar)
			{
					level++;
			}
			else if(stringCharAt == levelDecreaseChar)
			{
				level--;
			}
			else if(stringCharAt == ',')
			{
				if(level == 0)
				{
					// avoid a crash when substring length = 0;
					if(lastSeenComma == (j-1))
					{
						throw new JSONParseException("Invalid JSON File.");
					}
					arrayContents.add(JSONParser.parseString(jsonStringFromObjectStart.substring(lastSeenComma, j)));
					lastSeenComma = j + 1;
				}
			}
		}
		// We never found the end of the array, so must be an....
		if(level >= 0)
		{
			throw new JSONParseException("Invalid JSON File.");
		}
		
		// In the loop we were parsing the JSON between the commas, since the last element isn't trailed by a comma, we will miss one element
		// unless we catch it here.
		arrayContents.add(JSONParser.parseString(jsonStringFromObjectStart.substring(lastSeenComma)));
		
		return arrayContents;
	}
}
